/*
 *    This program is free software; you can redistribute it and/or modify
 *    it under the terms of the GNU General Public License as published by
 *    the Free Software Foundation; either version 2 of the License, or
 *    (at your option) any later version.
 *
 *    This program is distributed in the hope that it will be useful,
 *    but WITHOUT ANY WARRANTY; without even the implied warranty of
 *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *    GNU General Public License for more details.
 *
 *    You should have received a copy of the GNU General Public License
 *    along with this program; if not, write to the Free Software
 *    Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 */

/*
 *    DatabaseResultListener.java
 *    Copyright (C) 1999 University of Waikato, Hamilton, New Zealand
 *
 */

package weka.experiment;

import weka.core.FastVector;
import weka.core.RevisionUtils;

import java.sql.DatabaseMetaData;
import java.sql.ResultSet;

/**
 * <!-- globalinfo-start --> Takes results from a result producer and sends them
 * to a database.
 * <p/>
 * <!-- globalinfo-end -->
 * 
 * @author Len Trigg (trigg@cs.waikato.ac.nz)
 * @version $Revision: 5125 $
 */
public class DatabaseResultListener extends DatabaseUtils implements
		ResultListener {

	/** for serialization */
	static final long serialVersionUID = 7388014746954652818L;

	/** The ResultProducer to listen to */
	protected ResultProducer m_ResultProducer;

	/** The name of the current results table */
	protected String m_ResultsTableName;

	/** True if debugging output should be printed */
	protected boolean m_Debug = true;

	/** Holds the name of the key field to cache upon, or null if no caching */
	protected String m_CacheKeyName = "";

	/** Stores the index of the key column holding the cache key data */
	protected int m_CacheKeyIndex;

	/** Stores the key for which the cache is valid */
	protected Object[] m_CacheKey;

	/** Stores the cached values */
	protected FastVector m_Cache = new FastVector();

	/**
	 * Returns a string describing this result listener
	 * 
	 * @return a description of the result listener suitable for displaying in
	 *         the explorer/experimenter gui
	 */
	public String globalInfo() {
		return "Takes results from a result producer and sends them to a "
				+ "database.";
	}

	/**
	 * Sets up the database drivers
	 * 
	 * @throws Exception
	 *             if an error occurs
	 */
	public DatabaseResultListener() throws Exception {

		super();
	}

	/**
	 * Prepare for the results to be received.
	 * 
	 * @param rp
	 *            the ResultProducer that will generate the results
	 * @throws Exception
	 *             if an error occurs during preprocessing.
	 */
	public void preProcess(ResultProducer rp) throws Exception {

		m_ResultProducer = rp;
		// Connect to the database and find out what table corresponds to this
		// ResultProducer
		updateResultsTableName(m_ResultProducer);
	}

	/**
	 * Perform any postprocessing. When this method is called, it indicates that
	 * no more results will be sent that need to be grouped together in any way.
	 * 
	 * @param rp
	 *            the ResultProducer that generated the results
	 * @throws Exception
	 *             if an error occurs
	 */
	public void postProcess(ResultProducer rp) throws Exception {

		if (m_ResultProducer != rp) {
			throw new Error("Unrecognized ResultProducer calling postProcess!!");
		}
		disconnectFromDatabase();
	}

	/**
	 * Determines if there are any constraints (imposed by the destination) on
	 * any additional measures produced by resultProducers. Null should be
	 * returned if there are NO constraints, otherwise a list of column names
	 * should be returned as an array of Strings. In the case of
	 * DatabaseResultListener, the structure of an existing database will impose
	 * constraints.
	 * 
	 * @param rp
	 *            the ResultProducer to which the constraints will apply
	 * @return an array of column names to which resutltProducer's results will
	 *         be restricted.
	 * @throws Exception
	 *             if an error occurs.
	 */
	public String[] determineColumnConstraints(ResultProducer rp)
			throws Exception {
		FastVector cNames = new FastVector();
		updateResultsTableName(rp);
		DatabaseMetaData dbmd = m_Connection.getMetaData();
		ResultSet rs;
		// gets a result set where each row is info on a column
		if (m_checkForUpperCaseNames) {
			rs = dbmd.getColumns(null, null, m_ResultsTableName.toUpperCase(),
					null);
		} else {
			rs = dbmd.getColumns(null, null, m_ResultsTableName, null);
		}
		boolean tableExists = false;
		int numColumns = 0;

		while (rs.next()) {
			tableExists = true;
			// column four contains the column name
			String name = rs.getString(4);
			if (name.toLowerCase().startsWith("measure")) {
				numColumns++;
				cNames.addElement(name);
			}
		}

		// no constraints on any additional measures if the table does not exist
		if (!tableExists) {
			return null;
		}

		// a zero element array indicates maximum constraint
		String[] columnNames = new String[numColumns];
		for (int i = 0; i < numColumns; i++) {
			columnNames[i] = (String) (cNames.elementAt(i));
		}

		return columnNames;
	}

	/**
	 * Submit the result to the appropriate table of the database
	 * 
	 * @param rp
	 *            the ResultProducer that generated the result
	 * @param key
	 *            The key for the results.
	 * @param result
	 *            The actual results.
	 * @throws Exception
	 *             if the result couldn't be sent to the database
	 */
	public void acceptResult(ResultProducer rp, Object[] key, Object[] result)
			throws Exception {

		if (m_ResultProducer != rp) {
			throw new Error(
					"Unrecognized ResultProducer calling acceptResult!!");
		}

		// null result could occur from a chain of doRunKeys calls
		if (result != null) {
			putResultInTable(m_ResultsTableName, rp, key, result);
		}
	}

	/**
	 * Always says a result is required. If this is the first call, prints out
	 * the header for the Database output.
	 * 
	 * @param rp
	 *            the ResultProducer wanting to generate the result
	 * @param key
	 *            The key for which a result may be needed.
	 * @return true if the result should be calculated.
	 * @throws Exception
	 *             if the database couldn't be queried
	 */
	public boolean isResultRequired(ResultProducer rp, Object[] key)
			throws Exception {

		if (m_ResultProducer != rp) {
			throw new Error(
					"Unrecognized ResultProducer calling isResultRequired!");
		}
		if (m_Debug) {
			System.err.print("Is result required...");
			for (int i = 0; i < key.length; i++) {
				System.err.print(" " + key[i]);
			}
			System.err.flush();
		}
		boolean retval = false;

		// Check the key cache first
		if (!m_CacheKeyName.equals("")) {
			if (!isCacheValid(key)) {
				loadCache(rp, key);
			}
			retval = !isKeyInCache(rp, key);
		} else {
			// Ask whether the results are needed
			retval = !isKeyInTable(m_ResultsTableName, rp, key);
		}

		if (m_Debug) {
			System.err.println(" ..." + (retval ? "required" : "not required")
					+ (m_CacheKeyName.equals("") ? "" : " (cache)"));
			System.err.flush();
		}
		return retval;
	}

	/**
	 * Determines the table name that results will be inserted into. If
	 * required: a connection will be opened, an experiment index table created,
	 * and the results table created.
	 * 
	 * @param rp
	 *            the ResultProducer
	 * @throws Exception
	 *             if an error occurs
	 */
	protected void updateResultsTableName(ResultProducer rp) throws Exception {

		if (!isConnected()) {
			connectToDatabase();
		}
		if (!experimentIndexExists()) {
			createExperimentIndex();
		}

		String tableName = getResultsTableName(rp);
		if (tableName == null) {
			tableName = createExperimentIndexEntry(rp);
		}
		if (!tableExists(tableName)) {
			createResultsTable(rp, tableName);
		}
		m_ResultsTableName = tableName;
	}

	/**
	 * Returns the tip text for this property
	 * 
	 * @return tip text for this property suitable for displaying in the
	 *         explorer/experimenter gui
	 */
	public String cacheKeyNameTipText() {
		return "Set the name of the key field by which to cache.";
	}

	/**
	 * Get the value of CacheKeyName.
	 * 
	 * @return Value of CacheKeyName.
	 */
	public String getCacheKeyName() {

		return m_CacheKeyName;
	}

	/**
	 * Set the value of CacheKeyName.
	 * 
	 * @param newCacheKeyName
	 *            Value to assign to CacheKeyName.
	 */
	public void setCacheKeyName(String newCacheKeyName) {

		m_CacheKeyName = newCacheKeyName;
	}

	/**
	 * Checks whether the current cache contents are valid for the supplied key.
	 * 
	 * @param key
	 *            the results key
	 * @return true if the cache contents are valid for the key given
	 */
	protected boolean isCacheValid(Object[] key) {

		if (m_CacheKey == null) {
			return false;
		}
		if (m_CacheKey.length != key.length) {
			return false;
		}
		for (int i = 0; i < key.length; i++) {
			if ((i != m_CacheKeyIndex) && (!m_CacheKey[i].equals(key[i]))) {
				return false;
			}
		}
		return true;
	}

	/**
	 * Returns true if the supplied key is in the key cache (and thus we do not
	 * need to execute a database query).
	 * 
	 * @param rp
	 *            the ResultProducer the key belongs to.
	 * @param key
	 *            the result key
	 * @return true if the key is in the key cache
	 * @throws Exception
	 *             if an error occurs
	 */
	protected boolean isKeyInCache(ResultProducer rp, Object[] key)
			throws Exception {

		for (int i = 0; i < m_Cache.size(); i++) {
			if (m_Cache.elementAt(i).equals(key[m_CacheKeyIndex])) {
				return true;
			}
		}
		return false;
	}

	/**
	 * Executes a database query to fill the key cache
	 * 
	 * @param rp
	 *            the ResultProducer the key belongs to
	 * @param key
	 *            the key
	 * @throws Exception
	 *             if an error occurs
	 */
	protected void loadCache(ResultProducer rp, Object[] key) throws Exception {

		System.err.print(" (updating cache)");
		System.err.flush();
		m_Cache.removeAllElements();
		m_CacheKey = null;
		String query = "SELECT Key_" + m_CacheKeyName + " FROM "
				+ m_ResultsTableName;
		String[] keyNames = rp.getKeyNames();
		if (keyNames.length != key.length) {
			throw new Exception("Key names and key values of different lengths");
		}
		m_CacheKeyIndex = -1;
		for (int i = 0; i < keyNames.length; i++) {
			if (keyNames[i].equalsIgnoreCase(m_CacheKeyName)) {
				m_CacheKeyIndex = i;
				break;
			}
		}
		if (m_CacheKeyIndex == -1) {
			throw new Exception("No key field named " + m_CacheKeyName
					+ " (as specified for caching)");
		}
		boolean first = true;
		for (int i = 0; i < key.length; i++) {
			if ((key[i] != null) && (i != m_CacheKeyIndex)) {
				if (first) {
					query += " WHERE ";
					first = false;
				} else {
					query += " AND ";
				}
				query += "Key_" + keyNames[i] + '=';
				if (key[i] instanceof String) {
					query += "'"
							+ DatabaseUtils.processKeyString(key[i].toString())
							+ "'";
				} else {
					query += key[i].toString();
				}
			}
		}
		ResultSet rs = select(query);
		while (rs.next()) {
			String keyVal = rs.getString(1);
			if (!rs.wasNull()) {
				m_Cache.addElement(keyVal);
			}
		}
		close(rs);
		m_CacheKey = (Object[]) key.clone();
	}

	/**
	 * Returns the revision string.
	 * 
	 * @return the revision
	 */
	public String getRevision() {
		return RevisionUtils.extract("$Revision: 5125 $");
	}
}
